//////////////////////////////////////////////////////////////////////////////////
//
// ActionScript 3.0 Common Library (MIT License)
// Copyright (c) 2009 AS3CommonCode | Tony Birleffi | http://www.mosaiceye.com
// 
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
//////////////////////////////////////////////////////////////////////////////////

package org.as3commoncode.utils
{
	import flash.display.DisplayObjectContainer;	

	/**
	 * This is a utility class with many useful methods for objects.
	 * 
	 * @author Tony Birleffi
	 * 
	 * TODO: Add help examples and fix formatting.
	 */
	public class ObjectUtil 
	{
		/**
         * Parse's an object into the correct format for actionscript.
         * 
         * @param 	object		object from the style sheet, ex. 'blue'.
         */
        public static function parseObject(object:Object = null):Object
        {
        	// Rip through the current object and determine what to do.
        	for(var it:String in object)
        	{
	        	
	        	if(object[it] is String) {
	        		
	        		var str:String = object[it] as String;
	        		
	        		var singleQuote:Boolean = StringUtil.beginsWith(str, "'");
		        	var doubleQuote:Boolean = StringUtil.beginsWith(str, '"');
		        	
		        	// String with single quotes or double quotes.
		        	if(singleQuote == true || doubleQuote == true) {
		        		str = StringUtil.trimCharacters(str);
		        	}else{
		        		var num:Number = StringUtil.parseStyleNumberString(str);
			        	if(num) object[it] = num;
			        }
			        
		        }
	        	
        	}
        	
        	// Return the modified object
        	return object;
        }
       
        
		/**
		 * Concatinates the properties of one object/array to another. 
		 * If you wish to only overwrite a small set of properties you can set vOverwrite to true and list all of the properties you do not want to overwrite in vSkip. 
		 * Conversly, if it is only two or three properties you want to set just set them item by item without using this function. 
		 * As this spits out an object, you will have to cast your return as an array if you wish to concatinate arrays.
		 * 
		 * @param master		the object to which you wish to add properties.
		 * @param slave			the object from which you wish to transfer properties to master.
		 * @param overwrite		[Optional] true = overwrite properties on the master if they exist on the slave // false (default) = no not overwrite properties on the master.
		 * @param skip			[Optional] list of properties as strings to ignore (ie. will not be added from slave to master).
		 * @return Object.
		 * @use	   {@code var concat:Object = ObjectUtil.concat(myObject, herObject, false, [ "id", "area" ]);}
		 */
		public static function concat(master:Object, slave:Object, overwrite:Boolean = false, skip:Array = null):Object
		{
			if(!skip) skip = [];
			
			// If strings, booleans, or numbers are passed, try to add them together as best we can.
			if (master is String) return master + slave.toString();
			else if (master is Number) return Number(master) + Number(slave);
			else if (master is Boolean) return Boolean(Number(master) + Number(slave));
			
			// Loop through slave properties adding them to the master.
			for(var i:String in slave)
			{
				// Declare variables.
				var totalSkip:Number = skip.length;
				var skipItem:Boolean = false;
				
				// Check for skip properties.
				for(var j:Number = 0; j < totalSkip; j++)
				{
					// If the current property is one of the properties to skip.
					if (skip[ j ] == i)
					{
						// Set the skip boolean.
						skipItem = true;
						
						// Stop this loop.
						break;
					}
				}
				
				// If we need to skip this property.
				if (skipItem)
					// Continue to the next iteration of the loop so that the property is not set.
					continue;
				
				// If the property exists already and we do not want to overwrite it.
				if (master[ i ] != null && !overwrite)
					// Continue to the next iteration of the loop so that the property is not set.
					continue;
				
				// If this property is an object/array but not a movieclip.
				if ((slave[ i ] is Object || slave[ i ] is Array) && !(slave[ i ] is DisplayObjectContainer))
				{
					// Add the empty object/array from which to start.
					master[ i ] = (slave[ i ] is Array) ? [] : {};
					
					// Recursively copy all of its properties so that we don't have any pointers.
					master[ i ] = ObjectUtil.concat(master[ i ], slave[ i ], overwrite, skip);
				}
				else 
					// Add from slave to master.
					master[ i ] = slave[ i ];
			}
			
			// If the toString functionality has not been extended for this object, swap the toString functions.
			if (master.toString() == "[object Object]" && slave.toString() != "[object Object]")
				master = slave;
			
			return master;
		}
		
		/**
		 * Returns a copy of the object/array. 
		 * This is useful in case the object contains properties which are arrays.
		 * In this case, not duplicating the object would simply add that property as a reference to the array property on the other object. 
		 * Thus, changes to the array property's data in either object would change the array property's data in the other object.
		 *
		 * @param object		the object upon which to work.
		 * @return Object.
		 */
		public static function clone(object:Object):Object
		{
			// Make sure nothing other than objects/arrays were passed because we are using untyped function parameters and an untyped function return.
			if (object is String || object is Number || object is Boolean) return object;
			
			// Declare variables.
			var temp:Object = (object is Array) ? [] : {};
			
			// Concat all data from the object passed, into an empty object.
			return ObjectUtil.concat(temp, object);
		}
		
		/**
		 * Adds multiple properties to an object and all child objects if there are any.
		 *
		 * @param object			the object upon which to work
		 * @param props			a list of name/value pairs to add (ex: [ ["name", "value"], ["name", "value"], ... ]).
		 * @param overwrite			[Optional] true = overwrite property values if the property already exists on the object. false = (default) do not overwrite properties that already exist on the object.
		 * @return Object.
		 * @use	   {@code var addProperties:Object = ObjectUtil.addProperties(myObject, vArray, false);}
		 */
		public static function addProperties(object:Object, props:Array, overwrite:Boolean):Object
		{
			// Declare variables.
			var total:Number = props.length;
			
			// Loop through the properties to add, making copies on the target object and add property to all nested objects.
			for(var k:Number = 0; k < total; k++) ObjectUtil.inheritProperty(object, props[ k ][ 0 ], props[ k ][ 1 ], overwrite);
			
			// Return the copy.
			return object;
		}
		
		/**
		 * Adds a property to an object and all child objects if there are any.
		 *
		 * @param object			the object upon which to work.
		 * @param prop				the name of the property to add.
		 * @param value			the value of the property to add.
		 * @param overwrite	 	[Optional] true = overwrite property values if the property already exists on the object. false = (default) do not overwrite properties that already exist on the object.
		 * @return Object.
		 * @use    {@code var inheritProperty:Object = ObjectUtil.inheritProperty(myObject, "id", id, true);}
		 */
		public static function inheritProperty(object:Object, prop:String, value:Object, overwrite:Boolean):Object
		{
			// If strings, booleans, or numbers are passed, try to add them together as best we can.
			if (typeof object == "string") return object + value.toString();
			else if (typeof object == "number") return Number(object) + Number(value);
			else if (typeof object == "boolean") return Boolean(Number(object) + Number(value));
			
			// Get all properties of the array
			for(var i:String in object)
			{
				// If this element in the object/array is an object/array... recursively copy all of its properties so that we don't have any pointers.
				if (Object(object[ i ]) != null) object[ i ] = ObjectUtil.inheritProperty(object[ i ], prop, value, overwrite);
			}
			
			// If the property doesn't already exist on the object and we wish to overwrite it... add it
			if (object[ prop ] == null && overwrite) object[ prop ] = value;
			
			// Return the copy.
			return object;
		}
		
		/**
		 * Creates a list of name/value pairs representing the properties of a source object where all of its properties are strings/numbers/booleans. 
		 * The output array looks like: [ ["name", "value], ["name", "value"], ... ].
		 * 
		 * @param source			the object from which to pull data
		 * @param collection 		an object to which to add properties (use an empty object to start this function off).
		 * @param restricted		[Optional] a list of restricted data that should not be added to the request object.
		 * @return Object.
		 * @use	   {@code var propList:Array = ObjectUtil.minePropertiesList(myObject, "id");}
		 */
		public static function minePropertiesList(source:Object, vRestrict:Array):Array
		{
			// Build an object with all of the properties on the data object except a restricted list.
			var propsObj:Object = ObjectUtil.mineProperties(source, {}, vRestrict);
			
			// Declare variables.
			var propsArray:Array = [];
			var k:Number = 0;
			
			// Build a list from the object with all of the props data.
			for(var j:String in propsObj)
			{
				// Add the property name and value into the array of properties.
				propsArray[ k ] = [ j, propsObj[ j ] ];
				
				// Increment.
				++k;
			}
			// Return the newly created array.
			return propsArray;
		}
		
		/**
		 * Creates an object where all of its properties are strings/numbers/booleans farmed from a source object that may have other nested objects/arrays.
		 * 
		 * @param source			the object from which to pull data.
		 * @param collection		an object to which to add properties (use an empty object to start this function off).
		 * @param restricted		[Optional] a list of restricted data that should not be added to the request object.
		 * @return Object.
		 * @use	   {@code var propList:Object = ObjectUtil.mineProperties(myObject, "id");}
		 */
		public static function mineProperties(source:Object, collection:Object, restricted:Array):Object
		{
			// Loop through the properties in the source object adding to the collection object.
			for(var i:String in source)
			{
				// If the current property is an object.
				if (Object(source[ i ]))
					// Recursively add all of this object's properties...
					collection = ObjectUtil.mineProperties(source[ i ], collection, restricted);
				// Otherwise, make sure that this is not a restricted property.
				else
				{
					// Declare variables
					var isStopped:Boolean = false;
					var total:Number = restricted.length;
					
					// Make sure that this property is not restricted.
					for(var j:Number = 0; j < total; j++)
					{
						// If the current property is restricted...
						if (restricted[ j ] == i)
						{
							// Remember to stop the outer loop.
							isStopped = true;
							
							// Break this loop.
							break;
						}
					}
					// If this property was not restricted and it doesn't already exist (specicifity), add it to the request object.
					if (!isStopped && (collection[ i ] == null)) collection[ i ] = source[ i ];
				}
			}
			// Return the populated request object.
			return collection;
		}
		
		/**
		 * Gets all of the first level objects that are properties of the specified object (ie. get all objects out of this object).
		 * 
		 * @param master		the object to farm.
		 * @param skip			a list of properties that should be skipped.
		 * @return Array.
		 * @use	   {@code var objArray:Array = ObjectUtil.mineObjects(myObject, false, [ "id", "area" ]);}
		 */
		public static function mineObjects(master:Object, skip:Array):Array
		{
			// Declare variables
			var skipLength:Number = skip.length;
			var list:Array = [];
			
			// Loop through the master object looking for properties that are not numbers, strings, booleans, or arrays.
			for (var i:String in master)
			{
				// Declare variables.
				var shouldContinue:Boolean = false;
				
				// If there are any properties to skip.
				if (skipLength > 0)
				{
					// Search through the skip array to see if the current property should be skipped.
					for(var j:Number = 0; j < skipLength; j++)
					{
						// If this property should be skipped, continue with the outer loop.
						if (skip[ j ] == i)
						{
							shouldContinue = true;
							break;
						}
					}
				}
				
				// If we should be skipping this property.
				if (shouldContinue) continue;
				
				// If this property is an object...
				if (master[ i ] is Array)
				{
					// Get any objects inside of this array.
					var total:Number = master[ i ].length;
					for(var h:Number = 0; h < total; h++)
					{
						// If this is an object, add it to the array...
						if (master[ i ][ h ] is Object)
							list.push(master[ i ][ h ]);
					}
				}
				// Save it to the list.
				else if (typeof master[ i ] == "object")
					list.push(master[ i ]);
			}
			// Return the list
			return list;
		}
		
		/**
		 * Determines if something is a single object or if it is a list of objects and returns out of that
		 * object/list the object that contains the property with optionally matching value
		 * 
		 * @param data			the object on which you expect the property to exist.
		 * @param property		the property that you which to check for.
		 * @param value			[Optional] the value to which you wish to limit property.
		 * @param matches	 	[Optional] true = (default) find objects that have the requested property and property value, false = find objects that do not have the requested property and property value.
		 * @return Array.
		 * @use	   {@code var propArray:Array = ObjectUtil.findByProperty(myObject, "id", 260, true);}
		 *
		 *	@usageNote		If you're looking to return all values that DO NOT have the property at all, set value to NULL and match to false.
		 */
		public static function findByProperty(data:Array, property:String, value:Number = 0, matches:Boolean = true):Array
		{
			if(data != null)
			{
				// Declare variables.
				var test:Boolean;
				var list:Array = [];
				var array:Array = new Array(data);
				var object:Object = Object(data);
				
				// If data is an array.
				if (array != null)
				{
					// Declare variables.
					var total:Number = data.length;
					
					// Loop through the array searching for the requested property.
					for(var i:Number = 0; i < total; i++)
					{
						// Declare variables.
						var current:Object = data[i];
						
						// Test the property.
						test = ObjectUtil.testProperty(current, property, value);
						
						// If it tested true and we want to save objects with matching properties,
						// or if it tested false and we want to save objects without matching properties add it to the array of matching data.
						if ((test && matches) || (!test && !matches)) list.push(current);
					}
				}
				// Otherwise, if the data is an object
				else if (object != null)
				{
					// Test the property.
					test = ObjectUtil.testProperty(data, property, value);
					
					// If it tested true and we want to save objects with matching properties,
					// or if it tested false and we want to save objects without matching properties add it to the array of matching data.
					if ((test && matches) || (!test && !matches)) list.push(data);
				}
				else
				{
					trace("Invalid Data Type: The data object being searched through must be an object or an array.");
					return null;
				}
				// Send back the list of matching properties.
				return list;
			}
			
			return null;	
		}
		
		/**
		 * Compare two objects to see if they are identical. 
		 * This is a recursive function so all nested objects/arrays will also be tested.
		 *
		 * @param master		object upon which to work.
		 * @param slave		object we're comparing to.
		 * @return Boolean.
		 */
		public static function identical(master:Object, slave:Object):Boolean
		{
			// If slave is a reference to master, they match.
			if (master == slave) return true;
			
			// Loop through the properties of this object, testing to see if they match.
			for(var i:String in master)
			{
				// If one of the properties doesn't match, return false.
				if (!testProperty(slave, i, master[ i ])) return false;
			}
			
			// If we got this far, everything matched.
			return true;
		}
		
		/**
		 * Determines if a property exists on an object 
		 * and optionally if it has the correct value.
		 * 
		 * @param data			object on which you expect the property to exist.
		 * @param property		property that you which to check for.
		 * @param value		[Optional] the value to which you wish to limit property.
		 * @return Boolean.
		 */
		public static function testProperty(data:Object, property:String, value:Number = 0):Boolean
		{
			// If the property exists.
			if (data[ property ] != null)
			{
				// If a value to match was passed.
				if (value != 0)
				{
					// If they match.
					if (data[ property ] == value) return true;
				}
				// Otherwise.
				else
					// The property matches.
					return true;
			}
			
			// If we got this far, the property does not match.
			return false;
		}
	}
}
